package orm

import (
	"animal/octopus/common"
	"animal/octopus/model"
	"time"

	"gorm.io/gorm"

	log "github.com/sirupsen/logrus"
)

// ------------------------------------------------------------------------------
//
// ------------------------------------------------------------------------------
func GetModuleList(db *gorm.DB, parent uint) ([]model.Module, error) {
	var modules []model.Module
	result := common.DB(db).Where("modules.parent = ?", parent).Find(&modules)
	if result.Error != nil {
		log.Error("database error")
		return nil, common.NewErrCode(common.ErrDatabaseError)
	}

	return modules, nil
}

// ------------------------------------------------------------------------------
//
// ------------------------------------------------------------------------------
func CreateModule(db *gorm.DB, param common.CreateModuleParam) (model.Module, error) {
	module := model.Module{
		Name:       param.Name,
		Desc:       param.Desc,
		CreateType: param.CreateType,
		ModuleType: param.ModuleType,
		Parent:     param.Parent,
		App:        param.App,
		Space:      param.Space,
		Data:       "",
		LastReport: "",
	}

	if err := common.DB(db).Transaction(func(tx *gorm.DB) error {
		// check module name existence
		if _, ok := common.FindItemByName[model.Module](tx, param.Name); ok {
			log.WithField("module", param.Name).Error("module already exists")
			return common.NewErrCode(common.ErrGeneralError)
		}

		var parent *model.Module = nil
		if param.Parent != 0 {
			var ok bool = false
			parent, ok = common.FindItemByID[model.Module](tx, param.Parent)
			if !ok { // check parent existence
				log.WithField("parent", param.Parent).Error("cannot find parent")
				return common.NewErrCode(common.ErrGeneralError)
			}
		}

		// assign position
		if parent == nil {
			module.Position = param.Name
		} else {
			module.Position = parent.Position + "/" + param.Name
		}

		// create module
		if result := tx.Create(&module); result.Error != nil {
			log.WithField("app", param.Name).Error("create module failed")
			return common.NewErrCode(common.ErrDatabaseError)
		}

		// update parent topo view
		if parent != nil {
			name := parent.Name + "-topo-view"
			topoView, ok := common.FindItemByName[model.TopoView](tx, name)
			if !ok {
				log.WithField("name", name).Error("cannot find topo view")
				return common.NewErrCode(common.ErrGeneralError)
			}

			err := tx.Model(topoView).Association("Modules").Append(&module)
			if err != nil {
				log.WithField("error", err).Error("append failed")
				return err
			}
		}

		return nil
	}); err != nil {
		log.Error("create module failed")
		return model.Module{}, err
	}

	log.WithField("module", param.Name).Info("create module success")

	return module, nil
}

// ------------------------------------------------------------------------------
//
// ------------------------------------------------------------------------------
func EditModule(db *gorm.DB, param common.EditModuleParam) error {
	if err := common.DB(db).Transaction(func(tx *gorm.DB) error {
		// check module existence
		module, ok := common.FindItemByID[model.Module](tx, param.ID)
		if !ok {
			log.WithField("module", param.ID).Error("cannot find module")
			return common.NewErrCode(common.ErrGeneralError)
		}

		// parent check
		if module.Parent == 0 { // root module
			if module.Parent != param.Parent {
				log.Error("root module's parent cannot be changed")
				return common.NewErrCode(common.ErrGeneralError)
			}

			if param.Name != module.Name {
				log.Error("root module's name cannot be changed")
				return common.NewErrCode(common.ErrGeneralError)
			}
		} else { // non-root module
			if param.Parent == 0 {
				log.Error("cannot changed module to root module")
				return common.NewErrCode(common.ErrGeneralError)
			}

			// check parent existence
			if _, ok := common.FindItemByID[model.Module](tx, param.Parent); !ok {
				log.WithField("parent", param.Parent).Error("cannot find parent")
				return common.NewErrCode(common.ErrGeneralError)
			}
		}

		// name check
		if module.Name != param.Name {
			if _, ok := common.FindItemByName[model.Module](tx, param.Name); ok {
				log.WithField("name", param.Name).Error("module name conflict")
				return common.NewErrCode(common.ErrGeneralError)
			}
		}

		// update default topo view if parent changed
		if module.Parent != param.Parent {
			oldTopoView, err := GetDefaultTopoView(tx, module.Parent)
			if err != nil {
				log.WithField("ID", module.Parent).
					Error("cannot find default topo view")
				return err
			}

			newTopoView, err := GetDefaultTopoView(tx, param.Parent)
			if err != nil {
				log.WithField("ID", param.Parent).
					Error("cannot find default topo view")
				return err
			}

			if err := tx.Model(oldTopoView).Association("Modules").Delete(module); err != nil {
				log.WithFields(log.Fields{
					"module":   module.ID,
					"topoView": oldTopoView.ID,
				}).Error("remove module association from topo view failed")
				return err
			}
			log.WithFields(log.Fields{
				"module":   module.ID,
				"topoView": oldTopoView.ID,
			}).Info("remove module association from topo view success")

			if err := tx.Model(newTopoView).Association("Modules").Append(module); err != nil {
				log.WithFields(log.Fields{
					"module":   module.ID,
					"topoView": newTopoView.ID,
				}).Error("append module association to topo view failed")
				return err
			}
			log.WithFields(log.Fields{
				"module":   module.ID,
				"topoView": newTopoView.ID,
			}).Info("append module association to topo view success")
		}

		module.Name = param.Name
		module.Desc = param.Desc
		module.Parent = param.Parent
		if err := tx.Save(&module).Error; err != nil {
			log.WithField("error", err).Error("update failed")
			return err
		}

		return nil
	}); err != nil {
		log.WithField("param", common.MustMarshal(param)).Error("edit module failed")
		return err
	}

	log.WithField("param", common.MustMarshal(param)).Info("edit module success")

	return nil
}

// ------------------------------------------------------------------------------
//
// ------------------------------------------------------------------------------
func DeleteModule(db *gorm.DB, param common.DeleteModuleParam) error {
	if err := common.DB(db).Transaction(func(tx *gorm.DB) error {
		module, ok := common.FindItemByID[model.Module](tx, param.ID)
		if !ok {
			log.WithField("module", param.ID).Error("cannot find module")
			return common.NewErrCode(common.ErrGeneralError)
		}

		// remove module association
		result := tx.Exec("DELETE FROM view_modules WHERE module_id = ?", param.ID)
		if result.Error != nil {
			log.WithField("error", result.Error).Error("clear association failed")
			return result.Error
		}

		// find all topo views of module
		var topoViews []model.TopoView
		if err := tx.Where("module_id = ?", param.ID).Find(&topoViews).Error; err != nil {
			log.WithField("error", err).Error("find topo view failed")
			return err
		}

		// remove topo view and association
		for _, topoView := range topoViews {
			err := tx.Model(&topoView).Association("Modules").Clear()
			if err != nil {
				log.WithFields(log.Fields{
					"view":  topoView.ID,
					"error": err,
				}).Error("clear association failed")
				return err
			}

			if err := tx.Unscoped().Delete(&topoView).Error; err != nil {
				log.WithField("view", topoView.ID).Error("remove topo view failed")
				return err
			}
		}

		// remove module
		if err := tx.Unscoped().Delete(&module).Error; err != nil {
			log.WithFields(log.Fields{
				"id":    param.ID,
				"error": err,
			}).Error("delete module failed")
			return common.NewErrCode(common.ErrDatabaseError)
		}

		return nil
	}); err != nil {
		log.WithField("ID", param.ID).Error("delete module failed")
		return err
	}

	log.WithField("module", param.ID).Info("delete module success")

	return nil
}

// ------------------------------------------------------------------------------
//
// ------------------------------------------------------------------------------
func UpdateModule(db *gorm.DB, moduleID uint) error {
	module, ok := common.FindItemByID[model.Module](common.DB(db), moduleID)
	if !ok {
		log.WithField("ID", moduleID).Error("cannot find module")
		return common.NewErrCode(common.ErrGeneralError)
	}

	db.Model(module).Update("UpdatedAt", time.Now())

	log.WithField("ID", moduleID).Info("update module success")

	return nil
}
